/*
 * Copyright (C) 2012 Tomas Shestakov. <http://code.google.com/p/jmmo/>
 */

package jmmo.engine.property

import org.scalatest.WordSpec
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.mock.MockitoSugar
import org.mockito.Mockito._
import org.mockito.Matchers._

/**
 * @author Tomas Shestakov
 */
class ObservableContainerSpec(container: ModifiableObservableContainer) extends WordSpec with ShouldMatchers with MockitoSugar {

  def this() = this(new ModifiableContainerSupport {
    val id = Identifier('name -> "container")
    override def toString = id.attributes('name).asInstanceOf[String]
  })

  val slave =  new ModifiableContainerSupport {
    val id = Identifier('name -> "slave", "one" -> 1)
    override def toString = id.attributes('name).asInstanceOf[String]
  }
  
  val prop1 = Property("a", Identifier('name -> 1, "one" -> 1))
  val prop2 = Property("b", Identifier('name -> 2, "two" -> 2))
  val prop3 = Property("c", Identifier('name -> 3, "one" -> 1, "two" -> 2))
  val prop4 = Property("d", Identifier('name -> 4))

  val f1: Identifiable.Filter = (obs) => obs.id.attributes.get("one") == Some(1)
  val f2: Identifiable.Filter = (obs) => obs.id.attributes.get("two") == Some(2)
  val f3: Identifiable.Filter = Identifiable.passAll

  val mockListener1 = mock[ChangeListeners.Listener]
  when(mockListener1.isDefinedAt(any())).thenReturn(true)
  val mockListener2 = mock[ChangeListeners.Listener]
  when(mockListener2.isDefinedAt(any())).thenReturn(true)
  val mockListener3 = mock[ChangeListeners.Listener]
  when(mockListener3.isDefinedAt(any())).thenReturn(true)
  val mockListener4 = mock[ChangeListeners.Listener]
  when(mockListener4.isDefinedAt(any())).thenReturn(true)

  "An ObservableContainer" should {

    "provide `addObservable` method to add child observable" in {
      container.addObservable(prop1)
      container.addObservable(prop2)
      container.addObservable(prop3)
      Set(container.observables.toSeq: _*) should be (Set(prop1, prop2, prop3))
    }

    "provide `serializedCopy` method which returns SilentObservable with same id, attributes and observables" in {
      val silentContainer = container.serializedCopy()
      silentContainer.id should  equal (container.id)
      silentContainer.observables.toSet should equal (Set(SilentObservableValue("a", Identifier('name -> 1, "one" -> 1)),
        SilentObservableValue("b", Identifier('name -> 2, "two" -> 2)), SilentObservableValue("c", Identifier('name -> 3, "one" -> 1, "two" -> 2))))
    }

    "provide `apply` method which returns child observable with specified id wrapped in Option" in {
      container.observable(Identifier('name -> 1, "one" -> 1)) should be (Some(prop1))
      container.observable(Identifier('name -> 2, "two" -> 2)) should be (Some(prop2))
      container.observable(Identifier('name -> 3, "one" -> 1, "two" -> 2)) should be (Some(prop3))
      container.observable(Identifier('name -> 4)) should be (None)
      container.observable(null) should be (None)
    }

    "provide `observables` method which returns Itarable with child observables" in {
      //using Set because elements order in `observables` is not defined
      Set(container.observables.toSeq: _*) should be (Set(prop1, prop2, prop3))
    }

    "provide `removeObservable` method to remove child observable" in {
      container.removeObservable(prop1)
      container.removeObservable(prop2)
      container.removeObservable(prop3)
      container.observables should be ('empty)
    }

    "provide `addChangeListener` method to add listener of Property change," which {

      "forces to generate AddObsEvent/RemoveObsEvent events on add or remove child observables" in {
        container.addChangeListener(mockListener4)
        container.addChangeListener(mockListener2, f2)
        container.addObservable(slave)
        verify(mockListener2, never()).apply(AddObsEvent(container, slave))
        verify(mockListener4).apply(AddObsEvent(container, slave))

        container.addChangeListener(mockListener1, f1)
        container.addChangeListener(mockListener2, f2)
        container.addChangeListener(mockListener3, f3)
        container.addChangeListener(mockListener4)

        slave.addObservable(prop1)
        verify(mockListener1).apply(AddObsEvent(slave, prop1, container))
        verify(mockListener2, never()).apply(AddObsEvent(slave, prop1, container))
        verify(mockListener3).apply(AddObsEvent(slave, prop1, container))
        verify(mockListener4).apply(AddObsEvent(slave, prop1, container))

        slave.addObservable(prop2)
        verify(mockListener1, never()).apply(AddObsEvent(slave, prop2, container))
        verify(mockListener2, never()).apply(AddObsEvent(slave, prop2, container))
        verify(mockListener3).apply(AddObsEvent(slave, prop2, container))
        verify(mockListener4).apply(AddObsEvent(slave, prop2, container))

        slave.addObservable(prop3)
        verify(mockListener1).apply(AddObsEvent(slave, prop3, container))
        verify(mockListener2, never()).apply(AddObsEvent(slave, prop3, container))
        verify(mockListener3).apply(AddObsEvent(slave, prop3, container))
        verify(mockListener4).apply(AddObsEvent(slave, prop3, container))

        slave.addObservable(prop4)
        verify(mockListener1, never()).apply(AddObsEvent(slave, prop4, container))
        verify(mockListener2, never()).apply(AddObsEvent(slave, prop4, container))
        verify(mockListener3).apply(AddObsEvent(slave, prop4, container))
        verify(mockListener4).apply(AddObsEvent(slave, prop4, container))
      }

      "forces to generate ChangePropertyEvent events on changing property values in child observables" in {
        prop1() = "aa"
        verify(mockListener1).apply(ChangePropertyEvent(prop1, "a", "aa", container, slave))
        verify(mockListener2, never()).apply(ChangePropertyEvent(prop1, "a", "aa", container, slave))
        verify(mockListener3).apply(ChangePropertyEvent(prop1, "a", "aa", container, slave))
        verify(mockListener4).apply(ChangePropertyEvent(prop1, "a", "aa", container, slave))

        prop2() = "bb"
        verify(mockListener1, never()).apply(ChangePropertyEvent(prop2, "b", "bb", container, slave))
        verify(mockListener2, never()).apply(ChangePropertyEvent(prop2, "b", "bb", container, slave))
        verify(mockListener3).apply(ChangePropertyEvent(prop2, "b", "bb", container, slave))
        verify(mockListener4).apply(ChangePropertyEvent(prop2, "b", "bb", container, slave))

        prop3() = "cc"
        verify(mockListener1).apply(ChangePropertyEvent(prop3, "c", "cc", container, slave))
        verify(mockListener2, never()).apply(ChangePropertyEvent(prop3, "c", "cc", container, slave))
        verify(mockListener3).apply(ChangePropertyEvent(prop3, "c", "cc", container, slave))
        verify(mockListener4).apply(ChangePropertyEvent(prop3, "c", "cc", container, slave))

        prop4() = "dd"
        verify(mockListener1, never()).apply(ChangePropertyEvent(prop4, "d", "dd", container, slave))
        verify(mockListener2, never()).apply(ChangePropertyEvent(prop4, "d", "dd", container, slave))
        verify(mockListener3).apply(ChangePropertyEvent(prop4, "d", "dd", container, slave))
        verify(mockListener4).apply(ChangePropertyEvent(prop4, "d", "dd", container, slave))
      }

      "replace old listener even new one is the same with same filter" in {
        container.addChangeListener(mockListener1, f1)
        prop1() = "aaa"
        verify(mockListener1).apply(ChangePropertyEvent(prop1, "aa", "aaa", container, slave))
      }

      "adding the same listener with new filter will produce first `removeChangeListener` then `addChangeListener` with new filter" in {
        container.addChangeListener(mockListener1, f2)
        prop1() = "aaaa"
        verify(mockListener1, never()).apply(ChangePropertyEvent(prop1, "aaa", "aaaa", container, slave))
      }
    }

    "provide `removeChangeListener` method to remove listener of Property change" in {
      container.removeChangeListener(mockListener1)
      container.removeChangeListener(mockListener2)
      container.removeChangeListener(mockListener3)
      container.removeChangeListener(mockListener4)

      container.removeObservable(container)
      verify(mockListener1, never()).apply(RemoveObsEvent(container, slave))
      verify(mockListener2, never()).apply(RemoveObsEvent(container, slave))
      verify(mockListener3, never()).apply(RemoveObsEvent(container, slave))
      verify(mockListener4, never()).apply(RemoveObsEvent(container, slave))
    }

  }
}
